﻿/*
   Copyright 2012 University of Southampton

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Principal;

namespace uk.ac.soton.ses.DAL
{
    public partial class Experiment
    {
        /// <summary>
        /// Make a copy of the experiment so it can be used separately
        /// </summary>
        /// <returns></returns>
        public Experiment Copy()
        {
            using (MemoryStream ms = new MemoryStream())
            {
                IFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(ms, this);
                ms.Seek(0, SeekOrigin.Begin);
                return (Experiment)binaryFormatter.Deserialize(ms);
            }
        }

        /// <summary>
        /// Return a parameter from this experiment with a valid name
        /// </summary>
        /// <param name="key">The name of the parameter</param>
        /// <returns>The parameter, or null if the lookup fails</returns>
        public ExperimentParameter LookupParameter(string key)
        {
            //Related objects are loaded automatically. We will load all related parameters.
            if (!this.ExperimentParameters.IsLoaded)
            {
                this.ExperimentParameters.Load();
            }

            return this.ExperimentParameters.FirstOrDefault(param => param.Name == key);
        }

        /// <summary>
        /// Return an array of parameters from this experiment matching a valid name
        /// </summary>
        /// <param name="key">The name of the parameter</param>
        /// <returns>An array of parameters matching the key</returns>
        public ExperimentParameter[] LookupParameters(string key)
        {
            if (!this.ExperimentParameters.IsLoaded)
            {
                this.ExperimentParameters.Load();
            }
            return this.ExperimentParameters.Where(param => param.Name == key).ToArray();
        }

        /// <summary>
        /// Update a parameter's value, or create a new one
        /// </summary>
        /// <param name="dbcontext">We need a dbcontext to do this</param>
        /// <param name="key">The parameter's name</param>
        /// <param name="newValue">The parameter's new value</param>
        /// <param name="create">Whether to create a new parameter if one doesn't exist with the requested name</param>
        /// <returns>The experiment parameter</returns>
        public ExperimentParameter SaveParameterValue(MaterialsMetadataContext dbcontext, string key, string newValue, bool create)
        {
            //Related objects are loaded automatically. We will load all related parameters.
            if (!this.ExperimentParameters.IsLoaded)
            {
                this.ExperimentParameters.Load();
            }

            //Lookup the parameter based on the key
            ExperimentParameter parameterToUpdate = this.LookupParameter(key);

            //If the parameter doesn't exist, create a new one and associate it with container
            if (parameterToUpdate == null)
            {
                if (create)
                {
                    return this.AddParameter(dbcontext, key, newValue);
                }
                else
                {
                    //If they don't want us to create a new one, we can't do anything more. Exception?
                    return null;
                }
            }
            else
            {
                //If we get to here, we have a parameter to update, so we'll set it
                parameterToUpdate.Value = newValue;
            }
            return parameterToUpdate;
        }

        /// <summary>
        /// Creates a new parameter with the passed name and value.
        /// It does not check whether a parameter with existing name exists. Use SaveParameterValue for that.
        /// </summary>
        /// <param name="dbcontext">A database context to perform the action.</param>
        /// <param name="key">The name of the new parameter</param>
        /// <param name="newValue">The value of the new parameter</param>
        /// <returns>The new parameter</returns>
        public ExperimentParameter AddParameter(MaterialsMetadataContext dbcontext, string key, string newValue)
        {
            ExperimentParameter parameterToUpdate = new ExperimentParameter();
            parameterToUpdate.Name = key;
            parameterToUpdate.Value = newValue;
            this.ExperimentParameters.Add(parameterToUpdate);
            return parameterToUpdate;
        }

        /// <summary>
        /// Create a new parameter with a URL as its value and save it under a parent parameter containing other reports.
        /// </summary>
        /// <param name="dbcontext">A database context to perform the action.</param>
        /// <param name="reportUrl">The URL to be saved which will be used to generate the report.</param>
        /// <returns>The new parameter</returns>
        public ExperimentParameter AddReportParameter(MaterialsMetadataContext dbcontext, string reportUrl)
        {
            ExperimentParameter savedReportParam = this.SaveParameterValue(dbcontext, "Saved report " + DateTime.Now, reportUrl, true);
            bool resultokay = savedReportParam!=null && this.AddToReportParameters(savedReportParam, dbcontext);
            return savedReportParam;
        }

        /// <summary>
        /// Copy an experiment to a template
        /// </summary>
        /// <param name="newOwner">The owner of the experiment</param>
        /// <param name="newName">The name for the new template</param>
        /// <param name="copySystem">Whether to copy HDC system parameters</param>
        /// <param name="dbcontext">We need a db context to do this</param>
        /// <returns>A new experiment with the specified name</returns>
        public Experiment CopyToNewTemplate(SecurityIdentifier newOwner, string newName, bool copySystem, MaterialsMetadataContext dbcontext)
        {
            Experiment templateExperiment = new Experiment();
            dbcontext.AddToExperiments(templateExperiment);
            templateExperiment.Name = newName;
            templateExperiment.User = dbcontext.GetUserOrCreate(newOwner);
            templateExperiment.Date = DateTime.Now;
            templateExperiment.DefaultBasePath = "None";
            templateExperiment.IsDeleted = true;
            templateExperiment.ExperimentType = dbcontext.ExperimentTypes.First(type => type.ID == (int)HDCExperimentTypes.Template);
            dbcontext.SaveChanges();

            //Copy parameters
            this.CopyParametersToExperiment(templateExperiment, copySystem, dbcontext);

            //Copy files routine would go here if required

            return templateExperiment;
        }

        /// <summary>
        /// Copies all the parameters to another experiment. Useful for copying parameters from a template, but could be used for other reasons.
        /// </summary>
        /// <param name="destExperiment">The destination experiment for the new parameters</param>
        /// <param name="copySystem">Whether to copy HDC system parameters</param>
        /// <param name="dbcontext">A db context so we can do this</param>
        public void CopyParametersToExperiment(Experiment destExperiment, bool copySystem, MaterialsMetadataContext dbcontext)
        {
            //Create a dictionary for tracking the new parameter IDs and their source <sourceID,destID>
            Dictionary<int, int> parameterMap = new Dictionary<int, int>();

            //Set source experiment rather than using 'this' (so function could easily be adapted later)
            Experiment sourceExperiment = this;

            //Get a list of parameters that we will copy
            List<ExperimentParameter> sourceParameters = dbcontext.ExperimentParameters.Where(param => param.Experiment.ID == sourceExperiment.ID).ToList();

            //Create copies of all of the source parameters
            foreach (ExperimentParameter sourceParam in sourceParameters)
            {
                //Check if it is a system parameter
                if (sourceParam.IsSystemParameter && !copySystem)
                {
                    //We were told not to copy system parameters
                    continue;
                }

                //Add it to the destination parameter
                ExperimentParameter newParam = sourceParam.CreateCopy(true);
                destExperiment.ExperimentParameters.Add(newParam);

                //We have to save to get the ID of the new Parameter
                dbcontext.SaveChanges();

                //Record the relationship between the source and destination as we will need it when creating the parameter nesting
                parameterMap.Add(sourceParam.ID, newParam.ID);
            }

            //Create the relationships between the parameters
            foreach (ExperimentParameter sourceParam in sourceParameters)
            {
                //Check if it is a system parameter
                if (sourceParam.IsSystemParameter && !copySystem)
                {
                    //We were told not to copy system parameters
                    continue;
                }

                //Load the links from the database
                if (!sourceParam.ExperimentParameterChildren.IsLoaded)
                {
                    sourceParam.ExperimentParameterChildren.Load();
                }

                foreach (ExperimentParameterLink sourceLink in sourceParam.ExperimentParameterChildren)
                {
                    int sourceParentID = sourceLink.ParentParameterID;
                    int sourceLinkedID = sourceLink.LinkedParameterID;

                    //Look up what the new parameter IDs were
                    int destParentID = parameterMap[sourceParentID];
                    int destLinkedID = parameterMap[sourceLinkedID];

                    //Create a new link
                    ExperimentParameterLink newLink = new ExperimentParameterLink();
                    newLink.ExperimentParameterParent = dbcontext.ExperimentParameters.First(param => param.ID == destParentID);
                    newLink.ExperimentParameterLinked = dbcontext.ExperimentParameters.First(param => param.ID == destLinkedID);
                    dbcontext.AddToExperimentParameterLinks(newLink);
                }
            }

            //We're done, save any outstanding changes
            dbcontext.SaveChanges();
        }

        /// <summary>
        /// Check whether a user has permissions to an experiment, i.e. is an owner, a listed user, or in a group that has permission
        /// </summary>
        /// <param name="identity">The identity of the user to check</param>
        /// <param name="dbcontext">A database context to use for loading data</param>
        /// <returns>True if the user has permission, otherwise False</returns>
        public bool HasPermission(WindowsIdentity identity, MaterialsMetadataContext dbcontext)
        {
            //Load the experiment's users
            IQueryable<User> expUsers = dbcontext.UserAccesses.Where(ace => ace.Experiment.ID == this.ID).Select(ace => ace.User);

            //Load the experiment's groups' users
            IQueryable<Group> expGroups = dbcontext.GroupAccesses.Where(ace => ace.Experiment.ID == this.ID).Select(ace => ace.Group);
            IQueryable<User> expGroupOwners = expGroups.Select(grp=>grp.User);
            IQueryable<User> expGroupUsers = expGroups.SelectMany(grp => grp.UserGroupMaps.Select(user => user.User));

            //Check for permissions
            WindowsPrincipal wp = new WindowsPrincipal(identity);
            bool access = false;
            access |= dbcontext.UserIsAdmin(identity.User);
            access |= this.IsOwner(identity, dbcontext);
            access |= expUsers.Any(user => user.UserID == identity.User.Value);
            access |= expGroupOwners.Any(grp => grp.UserID == identity.User.Value);
            access |= expGroupUsers.Any(user => user.UserID == identity.User.Value);
            return access;
        }

        /// <summary>
        /// Check whether a user is the owner of the experiment
        /// </summary>
        /// <param name="identity">A WindowsIdentity object to compare against the database's security data.</param>
        /// <param name="dbcontext">A database context to use for performing the action.</param>
        /// <returns>True if the passed identity is the owner of the experiment, otherwise False.</returns>
        public bool IsOwner(WindowsIdentity identity, MaterialsMetadataContext dbcontext)
        {
            //Reload the experiment in case we're detached from the db context
            Experiment exp = dbcontext.GetExperimentByID(this.ID);
            exp.UserReference.Load();

            //Check owner
            WindowsPrincipal wp = new WindowsPrincipal(identity);
            bool owner = false;
            owner |= wp.IsInRole(new SecurityIdentifier(exp.User.UserID));
            if (exp.FileSystemUserID != null)
                owner |= wp.IsInRole(new SecurityIdentifier(exp.FileSystemUserID));
            return owner;
        }

        /// <summary>
        /// Finds the parameter that contains the URL of an experiment's thumbnail, if it is set.
        /// </summary>
        /// <param name="dbcontext">A database context to perform the action.</param>
        /// <returns>A string containing the URL of the experiment's thumbnail, otherwise null if not set.</returns>
        public string GetThumbnail(MaterialsMetadataContext dbcontext)
        {
            ExperimentParameter thumbParam = this.LookupParameter("HDCSystem.Thumbnail");
            if (thumbParam != null)
            {
                return thumbParam.Value;
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Finds the parent parameter used to store system related parameters which will be hidden from the user unless the user chooses to display them.
        /// </summary>
        /// <param name="create">Whether to create the system parameter parent if it does not exist</param>
        /// <param name="dbcontext">A database context to perform the action.</param>
        /// <returns>The system parameter container or null if it doesn't exist and create is set to false.</returns>
        public ExperimentParameter GetSystemParameter(bool create, MaterialsMetadataContext dbcontext)
        {
            ExperimentParameter[] systemParameters = this.LookupParameters("HDCSystem");

            //Ensure we only have one system parameter
            switch (systemParameters.Count())
            {
                //System parameter does not exist
                case 0:
                    //Return null if we weren't asked to create one
                    if (create)
                    {
                        return this.SaveParameterValue(dbcontext, "HDCSystem", String.Empty, true);
                    }
                    else
                    {
                        return null;
                    }

                //We have one system parameter we can return
                case 1:
                    return systemParameters[0];

                //Something went wrong as we have more than one system parameter.
                //We can return the first one and things will continue normally.
                default:
                    return systemParameters[0];
            }
        }

        /// <summary>
        /// Finds the parent parameter used to store reporting related parameters.
        /// </summary>
        /// <param name="create">Whether to create the reporting parameter parent if it does not exist.</param>
        /// <param name="dbcontext">A database context to perform the action.</param>
        /// <returns>The report parameter container or null if it doesn't exist and create is set to false.</returns>
        public ExperimentParameter GetReportParentParameter(bool create, MaterialsMetadataContext dbcontext)
        {
            ExperimentParameter[] reportParameters = this.LookupParameters("Saved reports");

            //Ensure we have only one parameter
            switch (reportParameters.Count())
            {
                //Report parameter does not exist
                case 0:
                    //Return null if we weren't asked to create one
                    if (create)
                    {
                        return this.SaveParameterValue(dbcontext, "Saved reports", "", true);
                    }
                    else
                    {
                        return null;
                    }
                //We have one parameter we can return
                case 1:
                    return reportParameters[0];

                //We have more than one parameter.
                //We return the first one to ensure the user can save their report and data is not lost.
                default:
                    return reportParameters[0];
            }
        }

        /// <summary>
        /// Move an existing parameter to the system parameter container.
        /// </summary>
        /// <param name="newSystemParameter">The existing parameter to make a system parameter.</param>
        /// <param name="dbcontext">A database context to perform the action.</param>
        public void AddToSystemParameters(ExperimentParameter newSystemParameter, MaterialsMetadataContext dbcontext)
        {
            ExperimentParameter systemParam = this.GetSystemParameter(true, dbcontext);
            systemParam.AddChildParameter(dbcontext, newSystemParameter);
        }

        /// <summary>
        /// Move an existing parameter to the report parameter container.
        /// </summary>
        /// <param name="newReportParameter">The existing parameter to make a report parameter.</param>
        /// <param name="dbcontext">A database context to perform the action.</param>
        /// <returns>True if successful, otherwise False.</returns>
        public bool AddToReportParameters(ExperimentParameter newReportParameter, MaterialsMetadataContext dbcontext)
        {
            ExperimentParameter reportParent = this.GetReportParentParameter(true, dbcontext);
            if (reportParent != null)
            {
                //Move the report parameter under the reports parent parameter
                reportParent.AddChildParameter(dbcontext, newReportParameter);
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Sets a data set's thumbnail to the passed URL.
        /// </summary>
        /// <param name="thumburl">A string containing the URL.</param>
        /// <param name="dbcontext">A database context to perform the action.</param>
        /// <returns>True if successful.</returns>
        public bool SetAsThumbnail(string thumburl, MaterialsMetadataContext dbcontext)
        {
            ExperimentParameter thumbnailParam = this.SaveParameterValue(dbcontext, "HDCSystem.Thumbnail", thumburl, true);
            this.AddToSystemParameters(thumbnailParam, dbcontext);
            return true;
        }

        /// <summary>
        /// Get the name of the experiment with its identifier
        /// </summary>
        /// <param name="idFirst">Whether the ID is placed before the name</param>
        /// <param name="prepend">A string to prepend</param>
        /// <param name="append">A string to append</param>
        /// <returns>A string in the format: "prepend" + "experiment_name [ID]" + "append"</returns>
        public string GetNameAndID(bool idFirst = false, string prepend="", string append="")
        {
            string returnString = "";
            if (idFirst)
            {
                returnString = string.Format("[{0}] {1}", this.ID, this.Name);
            }
            else
            {
                returnString = string.Format("{1} [{0}]", this.ID, this.Name);
            }

            return prepend + returnString + append;
        }

        /// <summary>
        /// Get the UNC path of the experiment, based on the configuration of the file system watcher that created the data set.
        /// This expects the local path of the file system watcher to match the data set's base path otherwise strange results might occur.
        /// If a file system watcher is to monitor a different path, it is expected that a new configuration is created rather than just changing the existing configuration to ensure this continues to work.
        /// </summary>
        /// <param name="dbcontext">A database context to perform the action.</param>
        /// <returns>A string containing a network path pointing to the data set's folder.</returns>
        public string GetUNCPath(MaterialsMetadataContext dbcontext)
        {
            try
            {
                //Load the file system watcher experiment associated with this experiment data file so we can lookup the web path
                HDCConfigurationOptionsFileSystemWatcher config = this.GetConfigurationOptionsFileSystemWatcher(dbcontext);
                string downloadPath = this.DefaultBasePath.Replace(config.LocalPath, config.UNCPath);
                return downloadPath;
            }
            catch (ConfigurationNotFoundException)
            {
                return null;
            }
        }

        /// <summary>
        /// Get the web path of the experiment, based on the configuration of the file system watcher that created the data set.
        /// This expects the web path of the file system watcher to match the data set's base path otherwise strange results might occur.
        /// If a file system watcher is to monitor a different path, it is expected that a new configuration is created rather than just changing the existing configuration to ensure this continues to work.
        /// </summary>
        /// <param name="dbcontext">A database context for performing the action.</param>
        /// <returns>A string containing a URL pointing to the data set's folder.</returns>
        public string GetWebPath(MaterialsMetadataContext dbcontext)
        {
            try
            {
                //Load the file system watcher experiment associated with this experiment data file so we can lookup the web path
                HDCConfigurationOptionsFileSystemWatcher config = this.GetConfigurationOptionsFileSystemWatcher(dbcontext);
                string downloadPath = this.DefaultBasePath.Replace(config.LocalPath, config.WebPath);
                return downloadPath;
            }
            catch (ConfigurationNotFoundException)
            {
                return null;
            }
        }

        /// <summary>
        /// Get the URL for the details page, used for editing a data set's metadata.
        /// </summary>
        /// <param name="dbcontext">A database context for performing the action.</param>
        /// <returns>A string containing a URL pointing to the data set's details page.</returns>
        public string GetDetailsWebPage(MaterialsMetadataContext dbcontext)
        {
            try
            {
                //Load the sharepoint configuration
                HDCConfigurationOptionsSharePoint spOptions = HDCConfiguration.GetConfigurationOptions().SharePointOptions;
                string detailsPage = spOptions.SiteCollectionUrl + HDCConfiguration.GetUrl_ExperimentDetails(this.ID);
                return detailsPage;
            }
            catch (ConfigurationNotFoundException)
            {
                return null;
            }
        }

        /// <summary>
        /// Return the configuration options for the file system watcher that created the data set.
        /// </summary>
        /// <param name="dbcontext">A database context for performing the action.</param>
        /// <returns>A HDCConfigurationOptionsFileSystemWatcher object with the configuration settings for the file system watcher.</returns>
        public HDCConfigurationOptionsFileSystemWatcher GetConfigurationOptionsFileSystemWatcher(MaterialsMetadataContext dbcontext)
        {
            //Load the file system watcher experiment associated with this experiment data file so we can lookup the web path
            Experiment optionsExperiment = dbcontext.Experiments.FirstOrDefault(exp => exp.ID == this.FileStoreOptionsExperimentID);
            //Experiment optionsExperiment = dbcontext.Experiments.Where(exp => exp.ID == this.ID).Select(exp => exp.FileStoreOptionsExperiment).FirstOrDefault();
            if (optionsExperiment != null)
            {
                HDCConfigurationOptionsFileSystemWatcher config = new HDCConfigurationOptionsFileSystemWatcher() { ConfigurationContainerID = optionsExperiment.ID };
                config.LoadConfigurationFromDatabase(dbcontext);
                return config;
            }
            else
            {
                throw new ConfigurationNotFoundException("Experiment does not have an associated file system watcher");
            }
        }

        /// <summary>
        /// Copies a file into the data set and waits for the file system watcher to find it and add it to the metadata database.
        /// </summary>
        /// <param name="sourceFileName">The path of the file to copy into the data set.</param>
        /// <param name="newFileName">The name of the new file to create in the data set.</param>
        /// <param name="dbcontext">A database context to perform the action.</param>
        /// <returns>The database record containing the database's view of the created file.</returns>
        /// <exception cref="System.IO.FileNotFoundException">Thrown if source file does not exist or watcher does not find copied file in a reasonable time.</exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">Thrown if the data set's folder cannot be found.</exception>
        /// <exception cref="System.IO.IOException">Thrown if the destination file already exists.</exception>
        public ExperimentDataFile AddFile(string sourceFileName, string newFileName, MaterialsMetadataContext dbcontext)
        {
            //Find out where the container is saved by getting the DefaultBasePath from the database
            string folderToSaveTo = this.DefaultBasePath;
            string fileNameToSaveTo = Path.Combine(folderToSaveTo, newFileName);

            //Split the path properly (to ensure slashes at end are correct)
            string basePath = Path.GetDirectoryName(fileNameToSaveTo);
            string fileName = Path.GetFileName(fileNameToSaveTo);

            Experiment exp = dbcontext.GetExperimentByID(this.ID);

            //Check file exists
            if (!File.Exists(sourceFileName))
            {
                throw new FileNotFoundException("File does not exist");
            }

            //Check directory exists
            if (!Directory.Exists(folderToSaveTo))
            {
                throw new DirectoryNotFoundException("Container doesn't exist.");
            }

            //Check the file doesn't already exist
            //Changed from using .ToList() .... df.GetFullPath() to ensure entire list of files are not transferred to us
            if (File.Exists(fileNameToSaveTo)
                || dbcontext.ExperimentDataFiles.Count(df => df.IsDeleted == false && df.BasePath == basePath && df.Filename == fileName) > 0)
            {
                throw new IOException("File already exists");
            }

            //We're now okay to copy the file
            File.Copy(sourceFileName, fileNameToSaveTo);

            //Wait for the record to appear in the database so we can return the file
            int check = 1;
            ExperimentDataFile newFile = null;
            while (check <= 10 && newFile == null)
            {
                exp.ExperimentDataFiles.Load(System.Data.Objects.MergeOption.OverwriteChanges);
                newFile = exp.ExperimentDataFiles.FirstOrDefault(df => !df.IsDeleted && df.GetFullPath().Equals(fileNameToSaveTo, StringComparison.OrdinalIgnoreCase));

                //If we didn't find a record yet, wait for a bit and then check again
                if (newFile == null)
                {
                    System.Threading.Thread.Sleep(1000);
                    check++;
                }
            }

            if (newFile == null)
            {
                string message = string.Format("Timed out waiting for synchronisation service. Try refreshing later. File: {0}.", fileNameToSaveTo);
                throw new FileNotFoundException(message);
            }

            //We found a matching record so return it
            return newFile;
        }
    }
}
